package config.center.core.zoo;

import config.center.core.annotation.CenterConfig;
import lombok.SneakyThrows;
import org.apache.curator.framework.recipes.cache.ChildData;
import org.apache.curator.framework.recipes.cache.NodeCache;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

/**
 * zookeeper config center bean post processor
 *
 * @author gaowenhao
 * @since 2021/9/17 10:35
 */
public class ZookeeperConfigCenterBeanPostProcessor implements BeanPostProcessor {

    private final ZookeeperManager zookeeperManager;

    private static final Map<String, NodeCache> cacheMap = new ConcurrentHashMap<>();

    public ZookeeperConfigCenterBeanPostProcessor(ZookeeperManager zookeeperManager) {
        this.zookeeperManager = zookeeperManager;
    }

    @Nullable
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @SneakyThrows
    @Nullable
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        Class<?> targetClass = AopUtils.getTargetClass(bean);
        Set<AccessibleObject> configMembers = Arrays.stream(targetClass.getDeclaredFields())
                .filter(field -> field.getAnnotation(CenterConfig.class) != null).collect(Collectors.toSet());
        configMembers.addAll(Arrays.stream(targetClass.getDeclaredMethods())
                .filter(method -> method.getAnnotation(CenterConfig.class) != null).collect(Collectors.toSet()));
        if (CollectionUtils.isEmpty(configMembers)) {
            return bean;
        }
        for (AccessibleObject member : configMembers) {
            CenterConfig config = member.getAnnotation(CenterConfig.class);
            String key = config.key();
            if (key.contains(" ")) {
                throw new IllegalArgumentException("syntax error, illegal center config key : \"" + key + "\"");
            }
            if (cacheMap.containsKey(key)) {
                throw new RuntimeException("duplicated center config key has been found, key : \"" + key + "\", key must be unique");
            }
            String path = zookeeperManager.keyToPath(key);
            boolean allowMiss = config.allowMiss();
            if (this.zookeeperManager.exist(path)) {
                String data = this.zookeeperManager.getData(path);
                this.populateBean(bean, member, data);
            } else if (!allowMiss) {
                throw new IllegalStateException("missed required center config : \"" + key + "\", add it to the config center");
            }
            NodeCache cache = new NodeCache(this.zookeeperManager.getCuratorFramework(), path, false);
            cache.getListenable().addListener(() -> {
                ChildData data = cache.getCurrentData();
                if(!Objects.isNull(data) && !Objects.isNull(data.getData())){
                    populateBean(bean, member, new String(data.getData()));
                }
            });
            cache.start(true);
            cacheMap.put(key, cache);
        }
        return bean;
    }


    /**
     * populate fields or invoke methods of bean
     * @param bean target bean
     * @param member one of bean's field or method
     * @param value the value to be populated or method parameter
     * @throws Exception exceptions when done
     */
    private void populateBean(Object bean, AccessibleObject member, String value) throws Exception {
        if (!Objects.isNull(bean)) {
            if (member instanceof Field) {
                this.fillField(bean, (Field) member, value);
            }
            if (member instanceof Method) {
                this.invokeMethod(bean, (Method) member, value);
            }
        }
    }


    private void fillField(Object bean, Field field, String value) throws Exception {
        ReflectionUtils.makeAccessible(field);
        if (Objects.isNull(value)) {
            field.set(bean, null);
            return;
        }
        Class<?> memberType = field.getType();
        if (Objects.equals(Integer.class, memberType)) {
            field.set(bean, Integer.parseInt(value));
        } else if (Objects.equals(Long.class, memberType)) {
            field.set(bean, Long.parseLong(value));
        } else if (Objects.equals(Float.class, memberType)) {
            field.set(bean, Float.parseFloat(value));
        } else if (Objects.equals(Double.class, memberType)) {
            field.set(bean, Double.parseDouble(value));
        } else if (Objects.equals(String.class, memberType)) {
            field.set(bean, value);
        } else if (Objects.equals(Boolean.class, memberType)) {
            field.set(bean, Boolean.parseBoolean(value));
        } else {
            throw new RuntimeException("not supported field type : \"" + memberType.getTypeName() + "\"");
        }
    }

    private void invokeMethod(Object bean, Method method, String value) throws Exception {
        int parameterNum = method.getParameterCount();
        if (parameterNum != 1) {
            throw new RuntimeException("not supported method parameter nums : \"" + parameterNum + "\"");
        }
        if (Objects.isNull(value)) {
            method.invoke(bean, (Object) null);
            return;
        }
        Class<?> parameterType = method.getParameterTypes()[0];
        if (Objects.equals(Integer.class, parameterType)) {
            method.invoke(bean, Integer.parseInt(value));
        } else if (Objects.equals(Long.class, parameterType)) {
            method.invoke(bean, Long.parseLong(value));
        } else if (Objects.equals(Float.class, parameterType)) {
            method.invoke(bean, Float.parseFloat(value));
        } else if (Objects.equals(Double.class, parameterType)) {
            method.invoke(bean, Double.parseDouble(value));
        } else if (Objects.equals(String.class, parameterType)) {
            method.invoke(bean, value);
        } else if (Objects.equals(Boolean.class, parameterType)) {
            method.invoke(bean, Boolean.parseBoolean(value));
        } else {
            throw new RuntimeException("not supported method parameter type : \"" + parameterType.getTypeName() + "\"");
        }
    }

}
